iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Software Development

30 篇淺談 Spring 框架的核心底層組件 系列 第 27 篇

Day 27: SpringBootCondition —— 條件裝配的模板骨架與評估機制

  • 分享至 

  • xImage
  •  

今天來講講 Spring boot「約定大於配置」背後最重要的判斷機制:條件裝配。一個 Spring boot 專案在啟動的時候,會讀取到上百個自動配置類別,但最後真正被註冊進 IoC 容器的,往往只有其中的一小部分,負責做出這個取捨的,就是今天要講的 SpringBootCondition。在講它之前,我們可以先簡單說說 Spring 框架原生的 Condition 介面:Condition 內部只定義了一個 matches 方法,參數分別是 ConditionContext(可以從中取得 BeanDefinition 註冊表、BeanFactory、Environment、ClassLoader ...等容器資訊)和 AnnotatedTypeMetadata(被標記的類別或方法的註解元資料),回傳值則是一個單純的 boolean。只要搭配 @Conditional 註解,Spring 在組件掃描、或 ConfigurationClassParser 解析配置類別的時候,就會在註冊 BeanDefinition 之前先呼叫 matches 判斷一次,回傳 false 就直接跳過註冊,像是 @Profile 這個註解,底層也是透過這套機制實現的。

SpringBootCondition 是 Spring boot 對 Condition 介面的一層抽象封裝,本身是一個抽象類別。它把 matches 方法宣告為 final,並在裡面定義了一套固定的模板流程:先呼叫子類別實作的 getMatchOutcome 取得判斷結果,再把結果記錄成日誌、寫進一個叫做 ConditionEvaluationReport 的評估報告裡,最後才回傳 boolean。這裡最大的差異在於 getMatchOutcome 的回傳值:它回傳的不是單純的 true / false,而是一個 ConditionOutcome 物件,除了「是否匹配」以外,還額外攜帶了一段 ConditionMessage,用來描述「為什麼匹配(或不匹配)」,例如 @ConditionalOnClass did not find required class 'xxx'。也因為每一次判斷都會留下原因,我們只要在設定檔加上 debug=true(或在啟動參數加上 --debug),Spring boot 就會在啟動時打印出一份 CONDITIONS EVALUATION REPORT,把所有自動配置分成 Positive matches 和 Negative matches 兩類,逐一列出生效或被排除的原因。當某個預期中的 Bean 沒有被創建時,這份報告幾乎是最直接的排查工具,這也是為什麼 Spring boot 在啟動失敗的時候,會特別提示我們 re-run your application with 'debug' enabled 的原因。

實務上,我們平常使用的 @ConditionalOnXxx 註解,都是 @Conditional 的組合註解,各自對應一個 SpringBootCondition 的實體子類別,例如 @ConditionalOnClass 對應 OnClassCondition、@ConditionalOnBean 和 @ConditionalOnMissingBean 對應 OnBeanCondition、@ConditionalOnProperty 對應 OnPropertyCondition、@ConditionalOnWebApplication 對應 OnWebApplicationCondition ...等,子類別只需要專注實作 getMatchOutcome,日誌、報告、例外處理等共用邏輯則統一收斂在父類別上。這裡可以提一個常見的疑問:如果 classpath 上根本沒有 Tomcat,為什麼 @ConditionalOnClass(Tomcat.class) 這種直接寫出類別的寫法,不會在啟動時拋出找不到類別的錯誤?原因是 Spring 在解析自動配置類別的時候,是透過 ASM 直接讀取 class 檔案的位元組碼,而不是用 ClassLoader 把類別載入 JVM,所以註解裡的 Tomcat.class 只會被當成一串類別名稱讀出來,後續 OnClassCondition 再拿著這串名稱,透過 ClassLoader 檢查類別存不存在。不過這個機制只對類別層級的註解有效,如果把 @ConditionalOnClass 寫在 @Bean 方法上,JVM 就有可能在條件判斷之前先載入方法的回傳型別,這時候比較建議把條件和 @Bean 方法一起拆到獨立的內部配置類別上,讓條件在類別層級先行判斷。

在所有的子類別裡,OnBeanCondition 是最需要小心使用的一個,原因跟判斷時機有關:Condition 有一個子介面叫做 ConfigurationCondition,它多定義了一個 getConfigurationPhase 方法,用來指定條件要在「解析配置類別」(PARSE_CONFIGURATION)還是「註冊 Bean」(REGISTER_BEAN)的階段判斷。OnBeanCondition 指定的是後者,因為「容器內有沒有某個 Bean」這件事,至少要等配置類別都解析完才有意義;但即便如此,它能看到的也只有「到目前為止已經註冊的 BeanDefinition」而已。這也是為什麼官方文件會強烈建議 @ConditionalOnBean / @ConditionalOnMissingBean 只用在自動配置類別上:自動配置類別會透過 DeferredImportSelector 延後到使用者的配置都處理完之後才解析,因此 @ConditionalOnMissingBean 可以穩定做到「使用者有自己定義,就跳過預設」的效果;但如果我們把它寫在一般的 @Configuration 或 @Component 上,判斷結果就會受到掃描順序的影響,時而成立、時而不成立,進而引發一些相當隱蔽的問題。如果自動配置之間也有先後依賴,則可以透過 @AutoConfiguration 的 after / before 屬性明確指定順序。


上一篇
Day 26: WebServerFactory —— 內嵌 Web 容器的創建與啟動時機
下一篇
Day 28: AutoConfigurationImportSelector —— 自動配置清單的篩選、排除與排序流程
系列文
30 篇淺談 Spring 框架的核心底層組件 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言